Undo, Redo

The Undo/Redo system in Vercengen is opt-in, and requires some initial architectural choices to be made for smoother integration into your program. Most Undo/Redo methods/classes are stored in the Delta Action Logging System (DALS).

Fundamentally, Undo/Redo is a tree system with multiple timelines. Each timeline is Git-like, with a Head state at [0], followed by a series of State Mutations from 1 to n. To activate Undo/Redo functionality, you must implement the following contract, or the linter will give you warnings and the ve.UndoRedo component will be non-functional.

ve.UndoRedo Components support both a Git-like Timeline view in addition to a scrollable Actions Map where the user can visually jump between different Timelines.

DALS Contract

The DALS contract is documented in ./UF/js/dals/Timeline_state.js, which requires the following static functions to be fully implemented on your program's end:

  • DALS.Timeline.loadState: function
    • arg0_json: Object|string
  • DALS.Timeline.saveState: function
    • arg0_json: Object|string

In addition to serialising/deserialising the state, DALS requires you to parse all actions in a JSON-serialisable manner via DALS.Timeline.parseAction(). Actions should be appended via new DALS.Action(json).

For Actions to be grouped together, they must share the same .key in their pushed JSON value.

  • DALS.Timeline.parseAction: function
    • arg0_json: Object|string

Example (Naissance):

The following example implementation comes from Naissance HGIS, and is functionally complete:

//State mutation functions
{
	DALS.Timeline.parseAction = function (arg0_json, arg1_do_not_push_action) {
		//Convert from parameters
		let json = (typeof arg0_json === "string") ? JSON.parse(arg0_json) : arg0_json;
		let do_not_push_action = arg1_do_not_push_action;
		
		//Initialise JSON
		if (json.options === undefined) json.options = {};
		if (json.value === undefined) json.value = [];
		
		//Iterate over multi-value packet (MVP) and filter it down to superclass single-value packets (SVPs)
		//console.log(json.value);
		for (let i = 0; i < json.value.length; i++) {
			if (json.value[i].type === "global") {
				if (json.value[i].load_save)
					DALS.Timeline.loadState(json.value[i].load_save);
				if (json.value[i].set_date) {
					main.date = json.value[i].set_date;
				} else if (json.value[i].refresh_date === true) {
					naissance.Geometry.instances.forEach((local_geometry) => local_geometry.draw());
				}
				continue;
			}
			if (json.value[i].type)
				naissance[json.value[i].type].parseAction(json.value[i]);
		}
		
		//Save action to current timeline if needed
		if (!do_not_push_action)
			new DALS.Action(json);
		
		//Force all UI_LeftbarHierarchy instances to .refresh()
		UI_LeftbarHierarchy.refresh();
	};
}

//State save/load functions
{
	DALS.Timeline.loadState = function (arg0_json) { //[WIP] - Finish function body
		//Convert from parameters
		let json = (arg0_json) ? arg0_json : {};
		if (typeof json === "string") json = JSON.parse(json);
		
		//Clear map first, then naissance.Geometry.instances
		for (let i = 0; i < naissance.Geometry.instances.length; i++)
			naissance.Geometry.instances[i].remove();
			
		scene.map_component.clear();
		naissance.Feature.instances = [];
		naissance.Geometry.instances = [];
		console.log(`DALS.Timeline.loadState called.`);
		
		//1. Handle naissance.Geometry classes
		//Iterate over json to load in each class
		Object.iterate(json, (local_key, local_value) => {
			if (local_value.class_name && local_value.type === "geometry") {
				let geometry_obj = new naissance[local_value.class_name]();
				if (local_value.id) geometry_obj.id = local_value.id;
				geometry_obj.history.fromJSON(local_value.history);
				try {
					if (geometry_obj.draw) geometry_obj.draw();
				} catch (e) { console.warn(e); }
			}
		});
		
		//2. Handle naissance.Feature classes
		Object.iterate(json, (local_key, local_value) => {
			if (local_value.class_name && local_value.type === "feature") {
				let feature_obj = new naissance[local_value.class_name]();
				if (local_value.id) feature_obj.id = local_value.id;
				if (local_value.value) feature_obj.json = local_value.value;
			}
		});
		for (let i = 0; i < naissance.Feature.instances.length; i++) {
			let local_feature = naissance.Feature.instances[i];
			
			local_feature.fromJSON(local_feature.json);
			try {
				if (local_feature.draw) local_feature.draw();
			} catch (e) { console.warn(e); }
		}
		
		//3. Force all UI_LeftbarHierarchy instances to .refresh()
		setTimeout(() => {
			UI_LeftbarHierarchy.refresh();
		}, 100);
		
		//Reload cursor
		main.layers.cursor_layer.addGeometry(main.brush.cursor)
	};
	
	DALS.Timeline.saveState = function () { //[WIP] - Finish function body for naissance.Feature
		//Declare local instance variables
		let json_obj = {};
		
		//Iterate over all naissance.Geometry.instances and serialise them
		for (let i = 0; i < naissance.Geometry.instances.length; i++) {
			let local_geometry = naissance.Geometry.instances[i];
			json_obj[local_geometry.id] = {
				id: local_geometry.id,
				class_name: local_geometry.class_name,
				history: local_geometry.history.toJSON(),
				type: "geometry"
			};
		}
		
		//Iterate over all naissance.Feature.instances and serialise them
		for (let i = 0; i < naissance.Feature.instances.length; i++) {
			let local_feature = naissance.Feature.instances[i];
			json_obj[local_feature.id] = {
				id: local_feature.id,
				class_name: local_feature.class_name,
				type: "feature",
				value: local_feature.toJSON()
			};
		}
		
		//Return statement
		return json_obj;
	};
}